/*
This is part of Screenbar, which is based in part on the Litestep
shell source code.

Copyright (C) 2008-2009 Jim Babcock
Copyright (C) 1997-2009 The LiteStep Development Team

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/
#include "stdafx.h"

vector<ScreenbarPanel*> panels;
HWND parentWindow = NULL;

/////////////////////////////////////////////////////////////////////////////

ScreenbarPanel::ScreenbarPanel(string prefix, Monitor *monitor, HWND parentWindow)
	:ScreenbarWindow(prefix, monitor)
{
	this->isEventHandler = false;

	hideAnimation = animationPool->getAnimation(getConfigString("HideAnimation", "", prefix.c_str()));
	showAnimation = animationPool->getAnimation(getConfigString("ShowAnimation", "", prefix.c_str()));
		
	bool vertical;
	
	alwaysOnTop = getConfigBool("AlwaysOnTop", false, prefix.c_str());
	isAppbar = getConfigBool("Appbar", false, prefix.c_str());

	if(isAppbar)
	{
		string screenEdgeStr = getConfigString("Edge", "bottom", prefix.c_str());
		if(!_stricmp(screenEdgeStr.c_str(), "top")) {
			screenEdge = ABE_TOP;
			vertical = false;
		} else if(!_stricmp(screenEdgeStr.c_str(), "left")) {
			screenEdge = ABE_LEFT;
			vertical = true;
		} else if(!_stricmp(screenEdgeStr.c_str(), "right")) {
			screenEdge = ABE_RIGHT;
			vertical = true;
		} else if(!_stricmp(screenEdgeStr.c_str(), "bottom")) {
			screenEdge = ABE_BOTTOM;
			vertical = false;
		} else {
			string message = retprintf("Invalid screen edge '%s' for panel '%s'.", screenEdgeStr.c_str(), prefix.c_str());
			warn(message.c_str());
			screenEdge = ABE_BOTTOM;
		}
		
		thickness = getConfigInt("Thickness", prefix.c_str());
		autoHide = getConfigBool("AutoHide", false, prefix.c_str());
		hiddenThickness = getConfigInt("HiddenThickness", 1, prefix.c_str());
		autoHideDelay = getConfigInt("HideDelay", 1000, prefix.c_str());
		
		visibleRect = getAppbarPos(NULL, false);
	}
	else
	{
		Rect monitorArea = monitor->getBoundingRect();
		
		visibleRect = Rect(
			monitorArea.left + getConfigCoord("X", monitorArea.width, prefix.c_str()),
			monitorArea.top + getConfigCoord("Y", monitorArea.height, prefix.c_str()),
			getConfigCoord("Width", monitorArea.width, prefix.c_str()),
			getConfigCoord("Height", monitorArea.height, prefix.c_str())
			);
		
		vertical = getConfigBool("Vertical", false, prefix.c_str());
		
		// Assign the panel a fake screen edge, based on whichever it's closest to
		if(visibleRect.top==0 && visibleRect.width>visibleRect.height)
			screenEdge = ABE_TOP;
		else if(visibleRect.top+visibleRect.height>=monitorArea.height && visibleRect.width>visibleRect.height)
			screenEdge = ABE_BOTTOM;
		else if(visibleRect.left==0 && visibleRect.height>visibleRect.width)
			screenEdge = ABE_LEFT;
		else if(visibleRect.left+visibleRect.width>=monitorArea.width && visibleRect.height>visibleRect.width)
			screenEdge = ABE_RIGHT;
		else
			screenEdge = -1;
		
		thickness = -1;
		autoHide = false;
	}
	
	if(animationDirection == directionUnspecified) {
		switch(screenEdge) {
			case ABE_TOP:    animationDirection = directionDown;  break;
			case ABE_BOTTOM: animationDirection = directionUp;    break;
			case ABE_RIGHT:  animationDirection = directionLeft;  break;
			case ABE_LEFT:   animationDirection = directionRight; break;
		}
	}
	
	ElementContext rootContext(this, monitor, NULL, NULL, vertical);
	initLayout(&rootContext);
	
	initWindow(WS_POPUP|WS_CLIPCHILDREN, parentWindow);
	
	DragAcceptFiles(window, TRUE);
	
	if(isAppbar)
	{
		appbarData.cbSize = sizeof(appbarData);
		appbarData.hWnd = window;
		appbarData.uEdge = screenEdge;
		appbarData.rc = visibleRect;
		appbarData.uCallbackMessage = UMSG_APPBAR;
		appbarData.lParam = NULL;
		SHAppBarMessage(ABM_NEW, &appbarData);
		
		if(autoHide)
		{
			HWND autoHideWindow = (HWND)SHAppBarMessage(ABM_GETAUTOHIDEBAR, &appbarData);
			if(autoHideWindow != NULL) {
				warn(retprintf(
					"Panel %s cannot auto-hide because there is already an auto-hiding appbar on the same edge.\n",
					prefix.c_str()).c_str());
				autoHide = false;
			} else {
				appbarData.lParam = TRUE;
				if(!SHAppBarMessage(ABM_SETAUTOHIDEBAR, &appbarData)) {
					warn(retprintf("Failed setting %s to auto-hide.\n", prefix.c_str()).c_str());
					autoHide = false;
				}
				appbarData.lParam = NULL;
			}
			
			SetTimer(window, TIMER_AUTOHIDE, autoHideDelay, NULL);
		}
		
		hidden = false;
		
		updateAppbarPos();
	}

	if(alwaysOnTop)
		SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	layout->update();

	registerEventHandlers();
	
	show();
}

ScreenbarPanel::~ScreenbarPanel()
{
	unregisterEventHandlers();
	
	if(window)
	{
		if(isAppbar)
			SHAppBarMessage(ABM_REMOVE, &appbarData);
		DragAcceptFiles(window, FALSE);
		ShowWindow(window, SW_HIDE);
	}
}

Rect ScreenbarPanel::getAppbarPos(const RECT *prevPos, bool hidden)
{
	RECT ret;
	
	if(prevPos) {
		// If there was another appbar on the same edge, Windows may have
		// adjusted this appbar's position, changing its thickness. Produce a
		// new rectangle, with the correct thickness.
		ret = *prevPos;
	} else {
		ret = monitor->getBoundingRect();
	}
	
	int appbarThickness = hidden ? hiddenThickness : thickness;
	
	switch(screenEdge) {
		case ABE_TOP:    ret.bottom = ret.top    + appbarThickness; break;
		case ABE_LEFT:   ret.right  = ret.left   + appbarThickness; break;
		case ABE_RIGHT:  ret.left   = ret.right  - appbarThickness; break;
		case ABE_BOTTOM: ret.top    = ret.bottom - appbarThickness; break;
	}
	
	return ret;
}

void ScreenbarPanel::hide()
{
	if(hidden)
		return;
	if(layout && !layout->allowAutoHide())
		return;
	
	hidden = true;
	
	if(hideAnimation)
		beginAnimation(hideAnimation, ANIM_HIDE);
	else
		updateAppbarPos();
}

void ScreenbarPanel::unhide()
{
	if(!isAppbar)
		return;
	
	SetTimer(window, TIMER_AUTOHIDE, autoHideDelay, NULL);

	if(!hidden)
		return;
	
	hidden = false;

	updateAppbarPos();
	
	if(showAnimation)
		beginAnimation(showAnimation, ANIM_SHOW);
}

void ScreenbarPanel::onFinishAnimation(int trigger)
{
	if(trigger == ANIM_HIDE) {
		updateAppbarPos();
	}
	
	ScreenbarWindow::onFinishAnimation(trigger);
}

void ScreenbarPanel::updateAppbarPos()
{
	// always make visibleRect non-hidden size
	appbarData.rc = getAppbarPos(NULL, false);
	SHAppBarMessage(ABM_QUERYPOS, &appbarData);
	visibleRect = getAppbarPos(&appbarData.rc, false);

	if(autoHide)
	{
		// always make ABM_SETPOS hidden size for autohide
		appbarData.rc = visibleRect;
		appbarData.rc = getAppbarPos(&appbarData.rc, true);
	}
	else
	{
		appbarData.rc = visibleRect;
	}
	
	SHAppBarMessage(ABM_SETPOS, &appbarData);

	Rect setRect = hidden ? appbarData.rc:visibleRect;
	SetWindowPos(window, NULL, setRect.left, setRect.top, setRect.width, setRect.height, SWP_NOZORDER | SWP_NOACTIVATE);
}

/////////////////////////////////////////////////////////////////////////////

void initPanels(HWND parentWindow)
{
	ScreenbarPanel::registerWindowClass();
	
	// Parse sbPanels, creating panelPrefixes, a list of prefixes each
	// of which corresponds to a ScreenbarPanel and has suffixes for window position
	// and settings.
	vector<string> panelPrefixes;
	tokenizeString(settings->panels, panelPrefixes, " \t,;");
	
	bool first = true;
	
	for(unsigned ii=0; ii<panelPrefixes.size(); ii++)
	{
		string prefix = panelPrefixes[ii];
		string monitorSpecifier = getConfigLine("Monitors", "all", prefix.c_str());
		set<Monitor*> monitors = findMonitors(monitorSpecifier);
		
		for(set<Monitor*>::iterator jj=monitors.begin(); jj!=monitors.end(); jj++)
		{
			Monitor *monitor = *jj;
			
			try {
				panels.push_back(new ScreenbarPanel(prefix, monitor, parentWindow));
			} catch(exception e) {
				warn(e.what());
			}
		}
	}
}

ScreenbarPanel *findPanel(int x, int y)
{
	for(unsigned ii=0; ii<panels.size(); ii++)
	{
		ScreenbarPanel *panel = panels[ii];
		if(panel->getRect().containsPoint(x, y))
			return panel;
	}
	return NULL;
}

ScreenbarPanel *getFirstPanel()
{
	return panels[0];
}

void getPanels(vector<ScreenbarPanel*> *outPanels)
{
	for(unsigned ii=0; ii<panels.size(); ii++)
		outPanels->push_back(panels[ii]);
}

void destroyPanels()
{
	for(unsigned ii=0; ii<panels.size(); ii++)
		delete panels[ii];
	panels.clear();
	ScreenbarPanel::unregisterWindowClass();
}

void refreshPanels()
{
	destroyPanels();
	
	delete layoutPool;
	layoutPool = new LayoutPool();
	
	initPanels(parentWindow);
}

void forceTrackerUpdate()
{
	windowTracker->update();
}

void updateLayouts()
{
	for(unsigned ii=0; ii<panels.size(); ii++)
		panels[ii]->checkForUpdates();
}

/////////////////////////////////////////////////////////////////////////////

LRESULT ScreenbarPanel::handleEvent(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg)
	{
		case WM_LBUTTONDOWN: return mouseButtonEvent(mouseLeft,   mouseDown, LOWORD(lParam), HIWORD(lParam));
		case WM_RBUTTONDOWN: return mouseButtonEvent(mouseRight,  mouseDown, LOWORD(lParam), HIWORD(lParam));
		case WM_MBUTTONDOWN: return mouseButtonEvent(mouseMiddle, mouseDown, LOWORD(lParam), HIWORD(lParam));
		case WM_LBUTTONUP:   return mouseButtonEvent(mouseLeft,   mouseUp,   LOWORD(lParam), HIWORD(lParam));
		case WM_RBUTTONUP:   return mouseButtonEvent(mouseRight,  mouseUp,   LOWORD(lParam), HIWORD(lParam));
		case WM_MBUTTONUP:   return mouseButtonEvent(mouseMiddle, mouseUp,   LOWORD(lParam), HIWORD(lParam));
		
		case WM_ACTIVATE: {
			if(isAppbar)
				SHAppBarMessage(ABM_ACTIVATE, &appbarData);
			
			if(autoHide && (wParam==WA_ACTIVE || wParam==WA_CLICKACTIVE))
				unhide();
			
			return 0;
		}
		
		case WM_TIMER: {
			if(wParam == TIMER_AUTOHIDE)
			{
				if(autoHide && !dragState.isDragging())
				{
					Point cursorPos = getCursorPos();
					if(!getRect().containsPoint(cursorPos.x, cursorPos.y))
						hide();
				}
				
				return 0;
			}
			break;
		}
		
		case WM_KILLFOCUS:
			if(dragState.isDragging())
				SetCapture(window);
			//if(drag && drag->active)
			//	cancelDrag();
			return 0;
			
		case UMSG_APPBAR:
			if(wParam == ABN_POSCHANGED)
			{
				updateAppbarPos();
			}
			else if(wParam == ABN_FULLSCREENAPP)
			{
				if(lParam) {
					SetWindowPos(window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
				} else if(alwaysOnTop) {
					SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
				} else {
					SetWindowPos(window, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
				}
			}
			return 0;
		
		case WM_MOUSEACTIVATE: {
			WindowData *foregroundWindow = windowTracker->getForegroundWindow();
			if(foregroundWindow)
				windowTracker->lastFocus = foregroundWindow->handle;
			else
				windowTracker->lastFocus = NULL;
			return MA_ACTIVATE;
		}
		
		case WM_MOUSEMOVE: {
			if(autoHide)
				unhide();
			
			// In window coordinates
			int mouseX = LOWORD(lParam);
			int mouseY = HIWORD(lParam);
			mouseMoveEvent(mouseX, mouseY);
			return 0;
		}
		case WM_MOUSELEAVE: {
			mouseLeaveEvent();
			checkForUpdates();
			return 0;
		}
		
		case WM_WINDOWPOSCHANGED: {
			// This is sent when the panel is moved, either by choice or by
			// some other program forcibly moving it. Update internals to
			// reflect the new size.
			LPWINDOWPOS windowPos = (LPWINDOWPOS)lParam;
			
			if(isAppbar)
				SHAppBarMessage(ABM_WINDOWPOSCHANGED, &appbarData);
			
			/*if( !(windowPos->flags & SWP_NOSIZE) ) {
				width = windowPos->cx;
				height = windowPos->cy;
			}
			if( !(windowPos->flags & SWP_NOMOVE) ) {
				x = windowPos->x;
				y = windowPos->y;
			}*/
			
			return DefWindowProc(window, uMsg, wParam, lParam);
		}
	}
	
	return ScreenbarWindow::handleEvent(uMsg, wParam, lParam);
}

LayoutReference *clickContext = NULL;

/// Called when a mouse button is pressed over this panel or released anywhere
/// after having been pressed on the panel. x and y are in window coordinates.
int ScreenbarPanel::mouseButtonEvent(MouseButton button, MouseAction action, int x, int y)
{
	bool draggedAndDropped = false;
	updateLayouts();
	
	LayoutReference *clickedRef = layout->elementAtPoint(x, y);
	LayoutCacheNode *clickedPos = NULL;
	if(clickedRef) clickedPos = clickedRef->getNode();
	
	LayoutCacheNode *pressNode=NULL, *releaseNode=NULL, *clickNode=NULL;

	if(clickedPos) {
		for(pressNode=clickedPos; pressNode; pressNode=pressNode->parent)
			if(pressNode->element->onPress[button]!="")
				break;
		for(releaseNode=clickedPos; releaseNode; releaseNode=releaseNode->parent)
			if(releaseNode->element->onRelease[button]!="")
				break;
		for(clickNode=clickedPos; clickNode; clickNode=clickNode->parent)
			if(clickNode->element->onClick[button]!="")
				break;
	} else {
		delete clickedRef;
	}
	
	string clickAction = "";
	
	if(action == mouseDown)
	{
		LayoutCacheNode *draggedNode = clickedPos;
		while(draggedNode && !draggedNode->element->draggable) {
			draggedNode = draggedNode->parent;
		}
		if(draggedNode) {
			Point offset = Point(x,y) - draggedNode->boundingRect.topLeft();
			dragState.anchorDrag(this, layout->getReferenceTo(draggedNode), offset);
			updateLayouts();
		}
	}
	else if(action == mouseUp)
	{
		if(dragState.isAnchored()) {
			dragState.cancelDrag();
		} else if(dragState.isDragging()) {
			draggedAndDropped = true;
			dragState.drop();
		}
	}
	
	bool commandsRun = false;
	
	if(clickNode && action==mouseDown)
	{
		lastClickLocation.context = clickNode->context;
		lastClickLocation.element = clickNode->element;
	}
	if(clickNode && action==mouseUp)
	{
		clickContext = layout->getReferenceTo(clickNode);
		
		if(lastClickLocation.context.match(clickNode->context)
		   && lastClickLocation.element == clickNode->element
		   && clickNode->element != NULL
		   && !draggedAndDropped)
		{
			LSExecute(window, clickNode->element->onClick[button].c_str(), SW_SHOWNORMAL);
			commandsRun = true;
		}
		lastClickLocation.element = NULL;
	
		delete clickContext;
		clickContext = NULL;
	}
	
	if(action==mouseDown && pressNode)
	{
		clickContext = layout->getReferenceTo(pressNode);
		commandsRun = true;
		
		LSExecute(window, pressNode->element->onPress[button].c_str(), SW_SHOWNORMAL);
		
		delete clickContext;
		clickContext = NULL;
	}
	else if(action==mouseUp && releaseNode && !commandsRun)
	{
		clickContext = layout->getReferenceTo(releaseNode);
		commandsRun = true;
		
		LSExecute(window, releaseNode->element->onRelease[button].c_str(), SW_SHOWNORMAL);
		
		delete clickContext;
		clickContext = NULL;
	}

	if(commandsRun)
	{
		// Running commands may have changed the layout
		updateLayouts();
	}
	
	delete clickedRef;
	return 0;
}

/// Called when the mouse moves over the panel, or moves anywhere while
/// dragging on a click that started inside the panel. Given in window
/// coordinates, but may outside the window and may be negative.
int ScreenbarPanel::mouseMoveEvent(int x, int y)
{
	// Suppress this event if outside window bounds
	if(x<0 || y<0 || x>=visibleRect.width || y>=visibleRect.height)
		return 1;
	
	// If something hovered has a tooltip specified, show it
	LayoutReference *hoveredRef = layout->elementAtPoint(x, y);
	LayoutCacheNode *hoveredNode = hoveredRef->getNode();
	delete hoveredRef;
	
	LayoutCacheNode *toolTipNode = hoveredNode;
	while(toolTipNode && toolTipNode->element->toolTip=="")
		toolTipNode = toolTipNode->parent;
	if(toolTipNode) {
		openToolTip(toolTipNode->element->toolTip, &toolTipNode->context, toolTipNode->boundingRect);
	} else {
		closeToolTip();
	}
	
	// Request notification when the mouse leaves the window
	TRACKMOUSEEVENT args;
		memset(&args, 0, sizeof(args));
		args.cbSize = sizeof(TRACKMOUSEEVENT);
		args.dwFlags = TME_LEAVE;
		args.hwndTrack = window;
	TrackMouseEvent(&args);
	
	// Sometimes receive this event while unprepared for it
	if(!layout)
		return 1;
	
	if(layout->updateHover())
		forceRedraw();
	
	if(dragState.isAnchored())
	{
		Point anchor = dragState.getAnchor();
		int dx = anchor.x - x;
		int dy = anchor.y - y;
		if(dx*dx + dy*dy > 25) {
			//SetCapture(window);
			dragState.beginDrag();
		}
	}
	else if(dragState.isDragging())
	{
		dragState.updateHover();
	}
	
	return 0;
}

void ScreenbarPanel::mouseLeaveEvent()
{
	if(dragState.isAnchored()) {
		//SetCapture(window);
		dragState.beginDrag();
	}
	
	closeToolTip();
}

void ScreenbarPanel::setVisible(bool visible)
{
	this->visible = visible;
	if(visible)
		ShowWindow(window, SW_SHOWNOACTIVATE);
	else
		ShowWindow(window, SW_HIDE);
}

void ScreenbarPanel::setAlwaysOnTop(bool onTop)
{
	trace << "setAlwaysOnTop("<<onTop<<")\n"; //DEBUG
	this->alwaysOnTop = onTop;
	
	DWORD oldStyle = GetWindowLong(window, GWL_STYLE);
	DWORD tempStyle = (oldStyle & ~WS_POPUP) | WS_CHILD;
	DWORD newStyle = (oldStyle & ~WS_CHILD) | WS_POPUP;
	
	SetWindowLong(window, GWL_STYLE, tempStyle);
	SetParent(window, onTop?NULL:getDesktopWindow());
	SetWindowLong(window, GWL_STYLE, newStyle);
	
	SetWindowPos(window, onTop?HWND_TOPMOST:HWND_NOTOPMOST,
		0, 0, 0, 0,
		SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE|SWP_NOSENDCHANGING);
}

bool ScreenbarPanel::getVisible()
	{ return visible; }
bool ScreenbarPanel::getAlwaysOnTop()
	{ return alwaysOnTop; }

Point ScreenbarPanel::getMenuPos(Rect buttonRect)
{
	Point ret;
	
	switch(getMenuEdge())
	{
		case ABE_LEFT:   ret = Point(buttonRect.getRight(), buttonRect.top); break;
		case ABE_RIGHT:  ret = Point(buttonRect.left, buttonRect.top); break;
		case ABE_BOTTOM: ret = Point(buttonRect.left, buttonRect.top); break;
		default:
		case ABE_TOP:    ret = Point(buttonRect.left, buttonRect.getBottom()); break;
	}
	
	ret += getTopLeft();
	return ret;
}

int ScreenbarPanel::getMenuEdge()
{
	return screenEdge;
}

string ScreenbarPanel::getMenuEdgeName()
{
	switch(getMenuEdge())
	{
		case ABE_LEFT:   return "left";
		case ABE_RIGHT:  return "right";
		case ABE_TOP:    return "top";
		case ABE_BOTTOM: return "bottom";
		default: return "";
	}
}
